/**
 * 
 */
package org.swing.utility.common.bytes;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayDeque;

/**
 * @author lqnhu
 *
 */
public class ByteBufferFifo {
	/** These buffers are available for reading (taking data). */
	private final ArrayDeque<ByteBuffer> readBuffers = new ArrayDeque<ByteBuffer>();
	/** This buffer is where data is written (putting data). */
	private ByteBuffer currentWrite;
	/**
	 * Cache of available ByteBuffers, since direct ByteBuffers are "expensive"
	 * to allocate.
	 */
	private final ArrayDeque<ByteBuffer> emptyBuffers = new ArrayDeque<ByteBuffer>();
	/**
	 * If we alternate reads and writes, this is used to save the read position
	 * of the buffer.
	 */
	private int savedReadPosition = 0;
	private boolean bigEndian = false;
	private static final int BUFFER_SIZE = 4096;

	public void clear() {
		readBuffers.clear();
		emptyBuffers.clear();
		currentWrite = null;
	}

	public ByteBuffer getWriteBuffer() {
		if (currentWrite == null) {
			// If we have a single read buffer, there may be space left in it
			// (combine writes!)
			if (readBuffers.size() == 1) {
				ByteBuffer lastRead = readBuffers.peekLast();
				if (!lastRead.hasRemaining()) {
					// The buffer is empty: remove it
					removeEmptyReadBuffer();
				} else if (lastRead.limit() < lastRead.capacity()) {
					// There is space! Remember its read position and make it a
					// write buffer
					readBuffers.removeLast();
					assert savedReadPosition == 0;
					savedReadPosition = lastRead.position();
					lastRead.position(lastRead.limit());
					lastRead.limit(lastRead.capacity());
					currentWrite = lastRead;
				}
			} else {
				// if more than 1 read buffer, the buffer is full
				assert readBuffers.isEmpty()
						|| readBuffers.peekLast().limit() == readBuffers
								.peekLast().capacity();
			}
			if (currentWrite == null) {
				// last resort: allocate a new buffer
				currentWrite = allocateBuffer();
			}
		} else if (currentWrite.remaining() == 0) {
			// buffer is full: get a new one
			/*
			 * TODO: Support flushing buffers early? Does this help performance?
			 * if (readBuffers.isEmpty()) { // no queued buffers yet: attempt to
			 * write it? flush(); }
			 */
			queueWriteBuffer();
			currentWrite = allocateBuffer();
			assert currentWrite.remaining() == currentWrite.capacity();
		}
		assert currentWrite.remaining() > 0;
		return currentWrite;
	}

	/** Returns the next ByteBuffer for reading or null if the FIFO is empty. */
	public ByteBuffer getReadBuffer() {
		ByteBuffer buffer = readBuffers.peekFirst();
		if (buffer != null && buffer.remaining() == 0) {
			// this buffer has been consumed: discard it and try the next
			removeEmptyReadBuffer();
			buffer = readBuffers.peekFirst();
			assert buffer == null || buffer.remaining() > 0;
		}
		if (buffer == null && currentWrite != null
				&& currentWrite.position() > 0) {
			// Last resort: there is data in our current write buffer: return it
			buffer = currentWrite;
			queueWriteBuffer();
		}
		assert buffer == null || buffer.remaining() > 0;
		return buffer;
	}

	private void removeEmptyReadBuffer() {
		ByteBuffer buffer = readBuffers.removeFirst();
		assert !buffer.hasRemaining();
		buffer.clear();
		emptyBuffers.add(buffer);
	}

	private void queueWriteBuffer() {
		// Flip and queue the write buffer
		currentWrite.flip();
		currentWrite.position(savedReadPosition);
		savedReadPosition = 0;
		assert currentWrite.remaining() > 0;
		readBuffers.add(currentWrite);
		// Delay allocation of write buffers: this lets us typically reuse one
		// buffer. We write to
		// it, flip it and read from it, then recycle it via the emptyBuffers
		// queue.
		currentWrite = null;
	}

	private ByteBuffer allocateBuffer() {
		ByteBuffer buffer = emptyBuffers.pollLast();
		if (buffer == null) {
			buffer = ByteBuffer.allocateDirect(BUFFER_SIZE);
			if (!bigEndian) {
				buffer.order(ByteOrder.nativeOrder());
			}
		}
		return buffer;
	}

	// TODO: This is a hack. Make a proper setByteOrder method?
	public void setBigEndian() {
		assert !bigEndian;
		bigEndian = true;
		@SuppressWarnings("rawtypes")
		ArrayDeque[] collections = { emptyBuffers, readBuffers };
		for (ArrayDeque<ByteBuffer> collection : collections) {
			for (ByteBuffer buffer : collection) {
				buffer.order(ByteOrder.BIG_ENDIAN);
			}
		}
		if (currentWrite != null) {
			currentWrite.order(ByteOrder.BIG_ENDIAN);
		}
	}
}
